/*
 * Copyright (C) SUSE LINUX Products GmbH 2012, all rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
 * License for more details.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>
#include <inttypes.h>

#include <curl/curl.h>

#include "ccan/list/list.h"
#include "dbg.h"
#include "base64.h"
#include "data_api.h"
#include "op.h"
#include "sign.h"
#include "util.h"
#include "conn.h"

/* convert base64 encoded key to binary and store in @econn */
int
elasto_conn_sign_setkey(struct elasto_conn *econn,
			const char *account,
			const char *key_b64)
{
	int ret;

	/* signing keys for S3 are set on econn initialisation */
	assert(econn->type == CONN_TYPE_AZURE);

	if (econn->sign.key_len > 0) {
		free(econn->sign.key);
		free(econn->sign.account);
		econn->sign.key_len = 0;
	}
	econn->sign.key = malloc(strlen(key_b64));
	if (econn->sign.key == NULL) {
		ret = -ENOMEM;
		goto err_out;
	}

	econn->sign.account = strdup(account);
	if (econn->sign.account == NULL) {
		ret = -ENOMEM;
		goto err_key_free;
	}

	ret = base64_decode(key_b64, econn->sign.key);
	if (ret < 0) {
		ret = -EINVAL;
		goto err_acc_free;
	}
	econn->sign.key_len = ret;
	dbg(1, "set account %s signing key: %s\n", account, key_b64);

	return 0;

err_acc_free:
	free(econn->sign.account);
err_key_free:
	free(econn->sign.key);
	econn->sign.key_len = 0;
err_out:
	return ret;
}

/*
 * @hdr_str:	single non-null terminated header string.
 * @num_bytes:	length of @hdr_str.
 */
static int
curl_hdr_process(struct op *op,
		 char *hdr_str,
		 uint64_t num_bytes)
{
	int ret;
	char *s;
	char *c = strchr(hdr_str, ':');
	char *val;
	int64_t len;

	if ((c == NULL) || (c > hdr_str + num_bytes) || (c == hdr_str)) {
		dbg(3, "ignoring header: %s\n", hdr_str);
		return 0;
	}

	for (s = c + 1; isspace((int)*s); s++);

	if (s > hdr_str + num_bytes) {
		dbg(0, "invalid header: %s\n", hdr_str);
		ret = -EINVAL;
		goto err_out;
	}

	len = num_bytes - (s - hdr_str);
	/* XXX curl makes no guarantees that hdr_str has a null term */
	val = strndup(s, (size_t)len);
	if (val == NULL) {
		ret = -ENOMEM;
		goto err_out;
	}

	/* trim trailing '\r', '\n' and space chars */
	for (s = val + len - 1; isspace((int)*s); s--);
	if (s < val) {
		dbg(0, "invalid header: %s\n", hdr_str);
		ret = -EINVAL;
		goto err_val_free;
	}
	*(s + 1) = '\0';

	*c = '\0';	/* zero terminate key */
	ret = op_rsp_hdr_add(op, hdr_str, val);
	if (ret < 0) {
		goto err_cln_fill;
	}

	if (!strcmp(hdr_str, "Content-Length")) {
		char *eptr;

		errno = 0;
		len = strtoll(val, &eptr, 10);
		if ((eptr == val) || (errno != 0)) {
			ret = -EINVAL;
			goto err_cln_fill;
		}

		if (op->rsp.write_cbs > 0) {
			dbg(0, "clen header received after data callback!\n");
			ret = -EINVAL;
			goto err_cln_fill;
		}
		/* allocate recv buffer in write callback */
		op->rsp.clen_recvd = true;
		op->rsp.clen = len;
	}

	ret = 0;
err_cln_fill:
	*c = ':';
err_val_free:
	free(val);
err_out:
	return ret;
}

static size_t
curl_hdr_cb(char *ptr,
	    size_t size,
	    size_t nmemb,
	    void *userdata)
{
	struct op *op = (struct op *)userdata;
	uint64_t num_bytes = (size * nmemb);
	int ret;

	ret = curl_hdr_process(op, ptr, num_bytes);
	if (ret < 0) {
		return 0;
	}

	return num_bytes;
}

static size_t
curl_read_cb(char *ptr,
	     size_t size,
	     size_t nmemb,
	     void *userdata)
{
	struct op *op = (struct op *)userdata;
	uint64_t num_bytes = (size * nmemb);
	uint64_t read_off;

	if ((op->req.data == NULL)
	 || ((op->req.data->type != ELASTO_DATA_IOV)
	      && (op->req.data->type != ELASTO_DATA_FILE))) {
		return -1;	/* unsupported */
	}

	op->req.read_cbs++;
	read_off = op->req.data->base_off + op->req.data->off;
	if (op->req.data->off + num_bytes > op->req.data->len) {
		dbg(3, "curl_read_cb buffer exceeded, len %" PRIu64
		       " off %" PRIu64 " io_sz %" PRIu64 ", capping\n",
		       op->req.data->len, op->req.data->off, num_bytes);
		num_bytes = op->req.data->len - op->req.data->off;
	}

	if (op->req.data->type == ELASTO_DATA_IOV) {
		memcpy(ptr, (void *)(op->req.data->iov.buf + read_off), num_bytes);
	} else if (op->req.data->type == ELASTO_DATA_FILE) {
		ssize_t ret;
		ret = pread(op->req.data->file.fd, ptr, num_bytes, read_off);
		if (ret != num_bytes) {
			dbg(0, "failed to read from file\n");
			return -1;
		}
	}
	op->req.data->off += num_bytes;
	return num_bytes;
}

/*
 * @cb_nbytes is the number of bytes provided by this callback, clen is the
 * number of bytes expected across all callbacks, but may not be known.
 */
static int
curl_write_alloc_err(struct op *op,
		     uint64_t cb_nbytes)
{
	struct op_rsp_error *err = &op->rsp.err;

	if ((err->buf != NULL) && (cb_nbytes > (err->len - err->off))) {
		uint8_t *buf;
		dbg(6, "extending error buffer by %" PRIu64 " bytes\n",
		    cb_nbytes);
		buf = realloc(err->buf, err->len + cb_nbytes);
		if (buf == NULL) {
			return -ENOMEM;
		}
		err->buf = buf;
		err->len += cb_nbytes;
	} else if (err->buf == NULL) {
		uint64_t sz = (op->rsp.clen_recvd ? op->rsp.clen : cb_nbytes);
		dbg(9, "allocating new %" PRIu64 " byte error buffer\n", sz);
		err->buf = malloc(sz);
		if (err->buf == NULL) {
			return -ENOMEM;
		}
		err->len = sz;
		err->off = 0;
	}
	return 0;
}

/*
 * @cb_nbytes is the number of bytes provided by this callback, clen is the
 * number of bytes expected across all callbacks, but may not be known.
 * FIXME this is too complicated! Clean it up by adding a higher layer callback
 * to the request structure and removing all data buffer types.
 */
static int
curl_write_alloc_std(struct op *op,
		     uint64_t cb_nbytes)

{
	int ret;
	uint64_t rem;

	dbg(9, "allocating buffer for %" PRIu64 " bytes\n", cb_nbytes);
	if (op->rsp.data == NULL) {
		uint64_t sz = (op->rsp.clen_recvd ? op->rsp.clen : cb_nbytes);
		/* requester wants us to allocate a recv iov */
		/* TODO check clen isn't too huge */
		ret = elasto_data_iov_new(NULL, sz, 0, true, &op->rsp.data);
		op->rsp.recv_cb_alloced = true;
		return ret;
	}

	switch (op->rsp.data->type) {
	case ELASTO_DATA_IOV:
		rem = (op->rsp.data->len - op->rsp.data->off);
		if (op->rsp.recv_cb_alloced == true) {
			if (op->rsp.clen_recvd) {
				dbg(0, "unexpected alloc call after clen\n");
			}
			if (cb_nbytes > rem) {
				dbg(2, "growing buf for callback\n");
				ret = elasto_data_iov_grow(op->rsp.data,
							     cb_nbytes - rem);
				if (ret < 0) {
					return ret;
				}
			}
			return 0;
		}
		/* external req buffer */
		if (op->rsp.clen_recvd && (op->rsp.clen
				+ op->rsp.data->base_off > op->rsp.data->len)) {
				dbg(0, "preallocated rsp buf not large enough "
				       "- alloced=%" PRIu64 ", "
				       "received clen=%" PRIu64 "\n",
				       op->rsp.data->len, op->rsp.clen);
			return -E2BIG;
		}
		/* FIXME check for space on !op->rsp.clen_recvd */
		break;
	case ELASTO_DATA_FILE:
		if (op->rsp.clen_recvd) {
			op->rsp.data->len = op->rsp.clen
						+ op->rsp.data->base_off;
		} else if (op->rsp.write_cbs == 1) {
			op->rsp.data->len = cb_nbytes + op->rsp.data->base_off;
		} else {
			op->rsp.data->len += cb_nbytes;
		}
		/* TODO, could fallocate entire file */
		break;
	default:
		assert(true);
		break;
	}

	return 0;
}

static int
curl_write_err(struct op *op,
	       uint8_t *data,
	       uint64_t num_bytes)
{
	dbg(8, "filling error buffer of len %" PRIu64 " at off %" PRIu64
	    " with %" PRIu64 " bytes\n",
	    op->rsp.err.len, op->rsp.err.off, num_bytes);
	if (op->rsp.err.off + num_bytes > op->rsp.err.len) {
		dbg(0, "fatal: error rsp buffer exceeded, "
		       "len %" PRIu64 " off %" PRIu64 " io_sz %" PRIu64 "\n",
		       op->rsp.err.len, op->rsp.err.off, num_bytes);
		return -E2BIG;
	}
	memcpy((void *)(op->rsp.err.buf + op->rsp.err.off), data,
	       num_bytes);
	op->rsp.err.off += num_bytes;

	return 0;
}

static int
curl_write_std(struct op *op,
	       uint8_t *data,
	       uint64_t num_bytes)
{
	int ret;
	uint64_t write_off = op->rsp.data->base_off + op->rsp.data->off;

	/* rsp buffer must have been allocated */
	assert(op->rsp.data != NULL);

	switch (op->rsp.data->type) {
	case ELASTO_DATA_IOV:
		if (write_off + num_bytes > op->rsp.data->len) {
			dbg(0, "fatal: curl_write_cb buffer exceeded, "
			       "len %" PRIu64 " off %" PRIu64 " io_sz %" PRIu64
			       "\n", op->rsp.data->len, write_off, num_bytes);
			return -E2BIG;
		}
		memcpy((void *)(op->rsp.data->iov.buf + write_off), data, num_bytes);
		break;
	case ELASTO_DATA_FILE:
		if (write_off + num_bytes > op->rsp.data->len) {
			dbg(0, "fatal: curl_write_cb file exceeded, "
			       "len %" PRIu64 " off %" PRIu64 " io_sz %" PRIu64
			       "\n", op->rsp.data->len, write_off, num_bytes);
			return -E2BIG;
		}
		ret = pwrite(op->rsp.data->file.fd, data, num_bytes, write_off);
		if (ret != num_bytes) {
			dbg(0, "file write io failed: %s\n", strerror(errno));
			return -EBADF;
		}
		break;
	default:
		assert(true);
		break;
	}
	op->rsp.data->off += num_bytes;
	return 0;
}

static size_t
curl_write_cb(char *ptr,
	      size_t size,
	      size_t nmemb,
	      void *userdata)
{
	struct op *op = (struct op *)userdata;
	uint64_t num_bytes = (size * nmemb);
	int ret;

	op->rsp.write_cbs++;
	dbg(9, "curl write cb %" PRIu64 "\n", op->rsp.write_cbs);
	/* alloc content buffer on the first callback, or if clen is unknown */
	if ((op->rsp.write_cbs == 1) || (op->rsp.clen_recvd == false)) {
		CURLcode cc;
		int ret_code;
		/* should already have the http response code */
		cc = curl_easy_getinfo(op->econn->curl, CURLINFO_RESPONSE_CODE,
				       &ret_code);
		if (cc != CURLE_OK) {
			dbg(0, "could not get response code in write cb\n");
			return -1;
		}

		op->rsp.err_code = ret_code;
		op->rsp.is_error = op_rsp_is_error(op->opcode, ret_code);

		if (op->rsp.is_error) {
			ret = curl_write_alloc_err(op, num_bytes);
		} else {
			ret = curl_write_alloc_std(op, num_bytes);
		}
		if (ret < 0) {
			dbg(0, "failed to allocate response buffer\n");
			return -1;
		}
	}

	if (op->rsp.is_error) {
		ret = curl_write_err(op, (uint8_t *)ptr, num_bytes);
	} else {
		ret = curl_write_std(op, (uint8_t *)ptr, num_bytes);
	}
	if (ret < 0) {
		return -1;
	}
	return num_bytes;
}

static size_t
curl_fail_cb(char *ptr,
	     size_t size,
	     size_t nmemb,
	     void *userdata)
{
	dbg(0, "Failure: server body data when not expected!\n");
	return 0;
}

/* a bit ugly, the signature src string is stored in @op for debugging */
static int
elasto_conn_send_prepare(struct elasto_conn *econn, struct op *op)
{
	int ret;
	struct curl_slist *http_hdr = NULL;
	struct op_hdr *hdr;
	char *url;

	curl_easy_setopt(econn->curl, CURLOPT_CUSTOMREQUEST, op->method);
	ret = asprintf(&url, "http%s://%s%s",
		 ((econn->insecure_http && !op->url_https_only) ? "" : "s"),
		 op->url_host, op->url_path);
	if (ret < 0) {
		return -ENOMEM;
	}
	curl_easy_setopt(econn->curl, CURLOPT_URL, url);
	free(url);
	curl_easy_setopt(econn->curl, CURLOPT_HEADERFUNCTION, curl_hdr_cb);
	curl_easy_setopt(econn->curl, CURLOPT_HEADERDATA, op);
	curl_easy_setopt(econn->curl, CURLOPT_WRITEDATA, op);
	curl_easy_setopt(econn->curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
	if (strcmp(op->method, REQ_METHOD_GET) == 0) {
		curl_easy_setopt(econn->curl, CURLOPT_HTTPGET, 1);
		curl_easy_setopt(econn->curl, CURLOPT_UPLOAD, 0);
		curl_easy_setopt(econn->curl, CURLOPT_INFILESIZE_LARGE, 0);
		curl_easy_setopt(econn->curl, CURLOPT_READFUNCTION,
				 curl_fail_cb);
	} else if ((strcmp(op->method, REQ_METHOD_PUT) == 0)
				|| (strcmp(op->method, REQ_METHOD_POST) == 0)) {
		uint64_t len = (op->req.data ? op->req.data->len : 0);
		/* INFILESIZE_LARGE sets Content-Length hdr */
		curl_easy_setopt(econn->curl, CURLOPT_INFILESIZE_LARGE, len);
		curl_easy_setopt(econn->curl, CURLOPT_UPLOAD, 1);
		curl_easy_setopt(econn->curl, CURLOPT_READDATA, op);
		curl_easy_setopt(econn->curl, CURLOPT_READFUNCTION,
				 curl_read_cb);
	} else if (strcmp(op->method, REQ_METHOD_HEAD) == 0) {
		/* No body component with HEAD requests */
		curl_easy_setopt(econn->curl, CURLOPT_HTTPGET, 0);
		curl_easy_setopt(econn->curl, CURLOPT_NOBODY, 1);
		curl_easy_setopt(econn->curl, CURLOPT_UPLOAD, 0);
		curl_easy_setopt(econn->curl, CURLOPT_INFILESIZE_LARGE, 0);
		curl_easy_setopt(econn->curl, CURLOPT_READFUNCTION,
				 curl_fail_cb);
	} else {
		/* DELETE, etc. must set Content-Length hdr */
		curl_easy_setopt(econn->curl, CURLOPT_INFILESIZE_LARGE, 0);
		curl_easy_setopt(econn->curl, CURLOPT_UPLOAD, 0);
	}

	if (op->req_sign != NULL) {
		ret = op->req_sign(econn->sign.account,
			       econn->sign.key, econn->sign.key_len,
			       op);
		if (ret < 0) {
			return ret;
		}
	}

	list_for_each(&op->req.hdrs, hdr, list) {
		char buf[512];
		struct curl_slist *http_hdr_nxt;
		ret = snprintf(buf, ARRAY_SIZE(buf), "%s: %s",
			       hdr->key, hdr->val);
		if (ret >= ARRAY_SIZE(buf)) {
			dbg(0, "header too large\n");
			return -E2BIG;
		}
		http_hdr_nxt = curl_slist_append(http_hdr, buf);
		if (http_hdr_nxt == NULL) {
			curl_slist_free_all(http_hdr);
			return -ENOMEM;
		}
		http_hdr = http_hdr_nxt;
	}
	curl_easy_setopt(econn->curl, CURLOPT_HTTPHEADER, http_hdr);

	return 0;	/* FIXME detect curl_easy_setopt errors */
}

int
elasto_conn_op_txrx(struct elasto_conn *econn,
		    struct op *op)
{
	int ret;
	CURLcode res;
	int redirects = 0;
	bool redirect;

	do {
		redirect = false;
		op->econn = econn;
		ret = elasto_conn_send_prepare(econn, op);
		if (ret < 0) {
			op->econn = NULL;
			return ret;
		}

		/* dispatch */
		res = curl_easy_perform(econn->curl);
		if (res != CURLE_OK) {
			dbg(0, "curl_easy_perform() failed: %s\n",
			       curl_easy_strerror(res));
			op->econn = NULL;
			return -EBADF;
		}

		if (op->rsp.write_cbs == 0) {
			/* write callback already sets this, otherwise still needed */
			curl_easy_getinfo(econn->curl, CURLINFO_RESPONSE_CODE,
					  &op->rsp.err_code);
			op->rsp.is_error = op_rsp_is_error(op->opcode,
							      op->rsp.err_code);
		}

		op->econn = NULL;
		ret = op_rsp_process(op);
		if (ret == -EAGAIN) {
			/* response is a redirect, resend */
			ret = op_req_redirect(op);
			if (ret < 0) {
				return ret;
			}
			if (++redirects > 2) {
				return -ELOOP;
			}
			redirect = true;
		} else if (ret < 0) {
			return ret;
		}
	} while (redirect);

	return 0;
}

static int
elasto_conn_init_common(bool insecure_http,
			struct elasto_conn **econn_out)
{
	uint32_t debug_level;
	struct elasto_conn *econn = malloc(sizeof(*econn));
	if (econn == NULL) {
		return -ENOMEM;
	}
	memset(econn, 0, sizeof(*econn));

	econn->curl = curl_easy_init();
	if (econn->curl == NULL) {
		free(econn);
		return -ENOMEM;
	}

	if (insecure_http) {
		dbg(1, "Using HTTP instead of HTTPS where available\n");
		econn->insecure_http = true;
	}

	debug_level = dbg_level_get();
	if (debug_level > 2) {
		curl_easy_setopt(econn->curl, CURLOPT_VERBOSE, 1);
	}
	*econn_out = econn;

	return 0;
}

int
elasto_conn_init_az(const char *pem_file,
		    const char *pem_pw,
		    bool insecure_http,
		    struct elasto_conn **econn_out)
{
	struct elasto_conn *econn;
	int ret;

	ret = elasto_conn_init_common(insecure_http, &econn);
	if (ret < 0) {
		return ret;
	}
	econn->type = CONN_TYPE_AZURE;
	curl_easy_setopt(econn->curl, CURLOPT_TCP_NODELAY, 1);
	curl_easy_setopt(econn->curl, CURLOPT_SSLCERTTYPE, "PEM");
	curl_easy_setopt(econn->curl, CURLOPT_SSLCERT, pem_file);
	curl_easy_setopt(econn->curl, CURLOPT_SSLKEYTYPE, "PEM");
	curl_easy_setopt(econn->curl, CURLOPT_SSLKEY, pem_file);
	if (pem_pw) {
		curl_easy_setopt(econn->curl, CURLOPT_KEYPASSWD, pem_pw);
	}
	*econn_out = econn;

	return 0;
}

/* signing keys are set immediately for S3 */
int
elasto_conn_init_s3(const char *id,
		    const char *secret,
		    bool insecure_http,
		    struct elasto_conn **econn_out)
{
	struct elasto_conn *econn;
	int ret;

	ret = elasto_conn_init_common(insecure_http, &econn);
	if (ret < 0) {
		goto err_out;
	}
	econn->type = CONN_TYPE_S3;
	econn->sign.key = (uint8_t *)strdup(secret);
	if (econn->sign.key == NULL) {
		ret = -ENOMEM;
		goto err_conn_free;
	}
	econn->sign.key_len = strlen(secret);

	econn->sign.account = strdup(id);
	if (econn->sign.account == NULL) {
		ret = -ENOMEM;
		goto err_secret_free;
	}

	*econn_out = econn;

	return 0;

err_secret_free:
	free(econn->sign.key);
err_conn_free:
	free(econn);
err_out:
	return ret;
}

void
elasto_conn_free(struct elasto_conn *econn)
{
	curl_easy_cleanup(econn->curl);
	if (econn->sign.key_len > 0) {
		free(econn->sign.key);
		free(econn->sign.account);
	}
	free(econn);
}

int
elasto_conn_subsys_init(void)
{
	CURLcode res;

	res = curl_global_init(CURL_GLOBAL_DEFAULT);
	if (res != CURLE_OK)
		return -ENOMEM;

	sign_init();

	return 0;
}

void
elasto_conn_subsys_deinit(void)
{
	curl_global_cleanup();
	sign_deinit();
}
